/* @flow */

const path = require('path')
const serialize = require('serialize-javascript')

import {isJS, isCSS} from '../util'
import TemplateStream from './template-stream'
import {parseTemplate} from './parse-template'
import {createMapper} from './create-async-file-mapper'
import type {ParsedTemplate} from './parse-template'
import type {AsyncFileMapper} from './create-async-file-mapper'

type
TemplateRendererOptions = {
  template? : string | (content
:
string, context
:
any
) =>
string;
inject ? : boolean;
clientManifest ? : ClientManifest;
shouldPreload ? : (file: string, type: string) => boolean;
shouldPrefetch ? : (file: string, type: string) => boolean;
serializer ? : Function;
}
;

export
type
ClientManifest = {
  publicPath: string;
  all: Array < string >;
  initial: Array < string >;
  async: Array < string >;
  modules: {
    [id: string
]:
Array < number >;
},
hasNoCssVersion ? : {
  [file: string
]:
boolean;
}
}
;

type
Resource = {
  file: string;
  extension: string;
  fileWithoutQuery: string;
  asType: string;
};

export default class TemplateRenderer {
  options: TemplateRendererOptions;
  inject: boolean;
  parsedTemplate: ParsedTemplate | Function | null;
  publicPath: string;
  clientManifest: ClientManifest;
  preloadFiles: Array<Resource>;
  prefetchFiles: Array<Resource>;
  mapFiles: AsyncFileMapper;
  serialize: Function;

  constructor(options: TemplateRendererOptions) {
    this.options = options
    this.inject = options.inject !== false
    // if no template option is provided, the renderer is created
    // as a utility object for rendering assets like preload links and scripts.

    const {template} = options
    this.parsedTemplate = template
      ? typeof template === 'string'
        ? parseTemplate(template)
        : template
      : null

    // function used to serialize initial state JSON
    this.serialize = options.serializer || (state => {
      return serialize(state, {isJSON: true})
    })

    // extra functionality with client manifest
    if (options.clientManifest) {
      const clientManifest = this.clientManifest = options.clientManifest
      // ensure publicPath ends with /
      this.publicPath = clientManifest.publicPath === ''
        ? ''
        : clientManifest.publicPath.replace(/([^\/])$/, '$1/')
      // preload/prefetch directives
      this.preloadFiles = (clientManifest.initial || []).map(normalizeFile)
      this.prefetchFiles = (clientManifest.async || []).map(normalizeFile)
      // initial async chunk mapping
      this.mapFiles = createMapper(clientManifest)
    }
  }

  bindRenderFns(context: Object) {
    const renderer: any = this
    ;['ResourceHints', 'State', 'Scripts', 'Styles'].forEach(type => {
      context[`render${type}`] = renderer[`render${type}`].bind(renderer, context)
    })
    // also expose getPreloadFiles, useful for HTTP/2 push
    context.getPreloadFiles = renderer.getPreloadFiles.bind(renderer, context)
  }

  // render synchronously given rendered app content and render context
  render(content: string, context: ?Object): string | Promise<string> {
    const template = this.parsedTemplate
    if (!template) {
      throw new Error('render cannot be called without a template.')
    }
    context = context || {}

    if (typeof template === 'function') {
      return template(content, context)
    }

    if (this.inject) {
      return (
        template.head(context) +
        (context.head || '') +
        this.renderResourceHints(context) +
        this.renderStyles(context) +
        template.neck(context) +
        content +
        this.renderState(context) +
        this.renderScripts(context) +
        template.tail(context)
      )
    } else {
      return (
        template.head(context) +
        template.neck(context) +
        content +
        template.tail(context)
      )
    }
  }

  renderStyles(context: Object): string {
    const initial = this.preloadFiles || []
    const async = this.getUsedAsyncFiles(context) || []
    const cssFiles = initial.concat(async).filter(({file}) => isCSS(file))
    return (
      // render links for css files
      (cssFiles.length
        ? cssFiles.map(({file}) => `<link rel="stylesheet" href="${this.publicPath}${file}">`).join('')
        : '') +
      // context.styles is a getter exposed by vue-style-loader which contains
      // the inline component styles collected during SSR
      (context.styles || '')
    )
  }

  renderResourceHints(context: Object): string {
    return this.renderPreloadLinks(context) + this.renderPrefetchLinks(context)
  }

  getPreloadFiles(context: Object): Array<Resource> {
    const usedAsyncFiles = this.getUsedAsyncFiles(context)
    if (this.preloadFiles || usedAsyncFiles) {
      return (this.preloadFiles || []).concat(usedAsyncFiles || [])
    } else {
      return []
    }
  }

  renderPreloadLinks(context: Object): string {
    const files = this.getPreloadFiles(context)
    const shouldPreload = this.options.shouldPreload
    if (files.length) {
      return files.map(({file, extension, fileWithoutQuery, asType}) => {
        let extra = ''
        // by default, we only preload scripts or css
        if (!shouldPreload && asType !== 'script' && asType !== 'style') {
          return ''
        }
        // user wants to explicitly control what to preload
        if (shouldPreload && !shouldPreload(fileWithoutQuery, asType)) {
          return ''
        }
        if (asType === 'font') {
          extra = ` type="font/${extension}" crossorigin`
        }
        return `<link rel="preload" href="${
          this.publicPath}${file
        }"${
          asType !== '' ? ` as="${asType}"` : ''
        }${
          extra
        }>`
      }).join('')
    } else {
      return ''
    }
  }

  renderPrefetchLinks(context: Object): string {
    const shouldPrefetch = this.options.shouldPrefetch
    if (this.prefetchFiles) {
      const usedAsyncFiles = this.getUsedAsyncFiles(context)
      const alreadyRendered = file => {
        return usedAsyncFiles && usedAsyncFiles.some(f => f.file === file)
      }
      return this.prefetchFiles.map(({file, fileWithoutQuery, asType}) => {
        if (shouldPrefetch && !shouldPrefetch(fileWithoutQuery, asType)) {
          return ''
        }
        if (alreadyRendered(file)) {
          return ''
        }
        return `<link rel="prefetch" href="${this.publicPath}${file}">`
      }).join('')
    } else {
      return ''
    }
  }

  renderState(context: Object, options

?:
  Object
):
  string {
  const {
  contextKey = 'state'
,
  windowKey = '__INITIAL_STATE__'
}
= options || {}
const state = this.serialize(context[contextKey])
const autoRemove = process.env.NODE_ENV === 'production'
  ? ';(function(){var s;(s=document.currentScript||document.scripts[document.scripts.length-1]).parentNode.removeChild(s);}());'
  : ''
const nonceAttr = context.nonce ? ` nonce="${context.nonce}"` : ''
return context[contextKey]
  ? `<script${nonceAttr}>window.${windowKey}=${state}${autoRemove}</script>`
  : ''
}

renderScripts(context
:
Object
):
string
{
  if (this.clientManifest) {
    const initial = this.preloadFiles.filter(({file}) => isJS(file))
    const async = (this.getUsedAsyncFiles(context) || []).filter(({file}) => isJS(file))
    const needed = [initial[0]].concat(async, initial.slice(1))
    return needed.map(({file}) => {
      return `<script src="${this.publicPath}${file}" defer></script>`
    }).join('')
  } else {
    return ''
  }
}

getUsedAsyncFiles(context
:
Object
):
  ? Array < Resource > {
  if(!context._mappedFiles && context._registeredComponents && this.mapFiles
)
{
  const registered = Array.from(context._registeredComponents)
  context._mappedFiles = this.mapFiles(registered).map(normalizeFile)
}
return context._mappedFiles
}

// create a transform stream
createStream(context
:
  ? Object
):
TemplateStream
{
  if (!this.parsedTemplate) {
    throw new Error('createStream cannot be called without a template.')
  }
  return new TemplateStream(this, this.parsedTemplate, context || {})
}
}

function normalizeFile(file: string): Resource {
  const withoutQuery = file.replace(/\?.*/, '')
  const extension = path.extname(withoutQuery).slice(1)
  return {
    file,
    extension,
    fileWithoutQuery: withoutQuery,
    asType: getPreloadType(extension)
  }
}

function getPreloadType(ext: string): string {
  if (ext === 'js') {
    return 'script'
  } else if (ext === 'css') {
    return 'style'
  } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) {
    return 'image'
  } else if (/woff2?|ttf|otf|eot/.test(ext)) {
    return 'font'
  } else {
    // not exhausting all possibilities here, but above covers common cases
    return ''
  }
}
